import processing.core.*;
import toxi.geom.*;
import oscP5.*;
import netP5.*;
import TUIO.*;
import processing.opengl.*;
import sojamo.drop.*;
import sftp.*;

import java.net.*;
import com.apple.dnssd.*;
import java.io.File;
import java.util.*;

/**
 * Welcome to The Tablet.  Its meant to be a tool to facilitate multi-user, multi-location, multi-touch, interaction.
 * The main application is responsible for doing these things:
 *  -maintaining an internal list of all active users on network
 *  -maintaining an internal list of all active pages on network
 *  -maintaining a list of pages we are members of
 *  -serving as a container for pages
 *  -managing its own configuration
 *  -synchronizing its configuration changes
 *  -registering key, tuio, and cursor events
 *  -sychronizing its key, tuio, cursor changes
 *  -handling saving and loading of pages as well as export formats (image and ?vector)
 */
public class thetablet extends PApplet {
  
  //This variable is used to track the number of connected clients we have

  private OscP5 oscP5;
  private int broadcastPort = 45678;
  private NetAddress broadcastAddress; // multicast port
  private NetAddressList peerAddressList; //all users broadcating their existance to broadcastAddress
  private ArrayList peerPages; //all pageID's listed in peer broadcasts
  private String ipInfo; //hold's this machine's ipAddress
  private Configuration myConfiguration;
  private String alias;
  
  private TuioProcessing tuioClient;
  
  private mainInterface gui;
  private PFont displayFont;
  private SDrop drop;
  private Sftp sftp;
  
  private Page[] pages;
  private Page activePage; //the page we're currently viewing
  private int configurationStatus; //0 if unconfigured, 1 if connected
  
  private long sessionID;
  
	public static void main(String args[]) {
	    PApplet.main(new String[] { "thetablet" });
	}
	
	
	public void setup() {
	  
	  //set the font
	  displayFont = loadFont("TimesNewRomanPSMT-48.vlw");
	  textFont(displayFont,14);
	  
	  //initialize the environment
	  size(1000,700,OPENGL);
	  frameRate(30);
	  
	  //establish the multitouch eventlisteners
	  tuioClient  = new TuioProcessing(this);
	  
	  //establish the client for handling image drop ins
	  drop = new SDrop(this);
	  
	  //initiate the list to store user pages
	  peerPages = new ArrayList(100);
	  
	  //get this computer's ip address
	  try {
      InetAddress addr = InetAddress.getLocalHost();
      ipInfo = addr.getHostAddress();
	  } catch (Exception e) { println("Failure obtaining IP."); }
	  
	  println("This machine's IP is " + ipInfo);
	  
	  myConfiguration = new Configuration(this,ipInfo,5,0,16,0);
	  gui = new mainInterface(this);

	  //broadcastAddress = new NetAddress("239.200.200.201",broadcastPort);
	  broadcastAddress = new NetAddress("127.0.0.1",broadcastPort);
	  
	  alias = ipInfo;
	}

	public void draw() {
	  
	  //broadcast presence
	  announceExistance();
	  
	  if (activePage != null) {
	    activePage.exist();
	  } else {
	    background(255);
	  }
	  
	  gui.exist();
	}
	
	
	
	
	
	/** The Sessioning Configuration is Handled Here **/
	
	public void createPage() {
    
    OscProperties myProperties = new OscProperties();
    myProperties.setDatagramSize(10000);
    myProperties.setListeningPort(broadcastPort);
    oscP5 = new OscP5(this, myProperties);
    
    activePage = new Page(this);  
    activePage.setPageID(ipInfo + "?" + day() + "." + minute() + "." + second());
    
    activePage.addConfiguration(myConfiguration);
    gui.setConfiguration(myConfiguration);
    
    println("Page created.");
    configurationStatus++;
   
	}
	
	public void joinPage(String ip) {
	  
	  //more ip parsing can be done later
	  if (ip.length() <= 9) {
	    gui.connectionStatus(false);
	    println("IP is bad.  Aborting");
	  }
	  
	  NetAddress selectedPeer = new NetAddress(ip,broadcastPort);
	  
	  //set up connection for receiving deltas
	  OscProperties myProperties = new OscProperties();
    myProperties.setDatagramSize(10000); 
    myProperties.setListeningPort(broadcastPort);
    oscP5 = new OscP5(this, myProperties);

	  //request connection to the session
	  OscMessage m = new OscMessage("/tablet/connect",new Object[0]);
	  m.add(myConfiguration.toString());
    oscP5.flush(m,selectedPeer);
    
  }
	
	public void joinPage() {
    
    //set up connection for receiving deltas
    OscProperties myProperties = new OscProperties();
    myProperties.setDatagramSize(10000); 
    myProperties.setListeningPort(broadcastPort);
    oscP5 = new OscP5(this, myProperties);

    //request connection to the session
    OscMessage m = new OscMessage("/tablet/connect",new Object[0]);
    m.add(myConfiguration.toString());
    //oscP5.flush(m,;
    
  }

  public void announceExistance() {
    //request connection to the session
    OscMessage m = new OscMessage("/tablet/exist",new Object[0]);
    m.add(alias);
    //for (int i = 0; i < pages.length; i++) {
    //  m.add(pages[i].getID());
    //}
    oscP5.flush(m,broadcastAddress);
  }
	

  public String getIP() {
    return ipInfo;
  }
	
	
  /** Called from the client to remove itself from the session **/
	public void disconnectSelf() {
	  OscMessage m = new OscMessage("/tablet/disconnect",new Object[0]);
    oscP5.send(m,broadcastAddress);  
	}
	
	
//called from the host to blast the most current configurations settings for all peers
  public void updateConfigList(String change) {
    if (change == "mode") {
      ArrayList tmpConfigs = activePage.getConfigurations();
      for(int i = 0; i < tmpConfigs.size(); i++) {
        Configuration cfg = (Configuration)tmpConfigs.get(i);
        OscMessage mess = new OscMessage("/tablet/configuration/mode");
        mess.add(cfg.getIP());
        mess.add(cfg.getMode());
        oscP5.send(mess, activePage.getNetAddresses());
        println("Changing Mode: " + cfg.getIP() + " , " + cfg.getMode());
      }
    }
  }
	
  
  public void updateConfigList() {
    ArrayList tmpConfigs = activePage.getConfigurations();
    for(int i = 0; i < tmpConfigs.size(); i++) {
      Configuration cfg = (Configuration)tmpConfigs.get(i);
      OscMessage mess = new OscMessage("/tablet/configuration");
      mess.add(cfg.getIP());
      mess.add(cfg.getMode());
      mess.add(cfg.getDrawWidth());
      mess.add(cfg.getDrawType());
      mess.add(cfg.getDrawColor());
      mess.add(cfg.getTextSize());
      mess.add(cfg.getTextColor());
      oscP5.send(mess, activePage.getNetAddresses());
    }
  }
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	/*************** OSC Receive Event ********************************************************************/
	public void oscEvent(OscMessage theOscMessage) {
	  
  	  /* check if the address pattern fits any of our patterns */
  	  if (theOscMessage.addrPattern().equals("/tablet/connect")) {
  	    
  	    String theIPaddress = theOscMessage.netAddress().address();
  	    
  	    //find out which page they want to connect to 
  	    
  	    //if we don't already have record of this client 
  	    if (activePage.getConfigurationByIP(theIPaddress) == null) {
  	      
  	      String tmp = theOscMessage.get(0).stringValue();
  	      
  	      //add their configuration to the page
  	      activePage.addConfiguration(new Configuration(this,tmp));
  	    
  	      println("Adding " + theIPaddress + " to the ip list.");
  	      
  	    } else {
  	      println("Error: " + theIPaddress + " is already connected.");
  	    }
  	   
  	    println("Currently there are " + activePage.getConfigurations().size() + " remote locations connected.");
  	    
  	    //send them the current page
  	    OscMessage curPage = new OscMessage("/tablet/page");
  	    curPage.add(activePage.toString());
  	    oscP5.send(curPage, theOscMessage.netAddress());
  	    
  	    //update clients' configurations lists
        updateConfigList("");
  	    
  	  } else if (theOscMessage.addrPattern().equals("/tablet/exist")) {
  	    
  	    NetAddress tmp = new NetAddress(theOscMessage.address(),broadcastPort);
  	    if (!peerAddressList.contains(tmp)) {
  	      peerAddressList.add(tmp);
  	    }
  	    
  	    
  	    
  	    //this implementation is probably bad and really winded
  	    int isDiff = -1;
  	    String tmpPageID = theOscMessage.get(0).stringValue();
  	    for(int i = 0; i < peerPages.size(); i++) {
  	      String tmpPeerPageID = (String)peerPages.get(i);
  	      if (!tmpPeerPageID.equalsIgnoreCase(tmpPageID)) {
  	       isDiff = i;
  	      }
  	    }
  	    
  	    if (isDiff > -1) {  peerPages.add((String)peerPages.get(isDiff)); }
  	    
  	    
  	    
  	    
  	  } else if (theOscMessage.addrPattern().equals("/tablet/leaving")) {
        
        NetAddress tmp = new NetAddress(theOscMessage.address(),broadcastPort);
        if (!peerAddressList.contains(tmp)) {
          peerAddressList.remove(tmp);
        }
        
      } else {
  	    
  	    parseDeltas(theOscMessage);
  	    
	    }
	}
	
	
	
	//this receives incoming information and modifies the program accordingly
	private void parseDeltas(OscMessage theOscMessage) {
	  
	  if (theOscMessage.addrPattern().contains("/tablet/configuration")) {
	    parseConfig(theOscMessage);
	  } else if (theOscMessage.addrPattern().contains("/tablet/key")) {
	    parseKey(theOscMessage);
	  } else if (theOscMessage.addrPattern().contains("/tablet/cursor")) {
	    parseCursor (theOscMessage);
	  } else if (theOscMessage.addrPattern().equals("/tablet/page")) {
	    if (configurationStatus == 0) {
	      parsePage(theOscMessage);
	    }
	  }
	  
	}


	private void parseConfig(OscMessage theOscMessage) {
	  
	  if(theOscMessage.addrPattern().equals("/tablet/configuration")) {
  	  if (configurationStatus == 2) {
    	  //most basic configuration update consists of all the values, only host sends this out
    	  String configIP = theOscMessage.get(0).stringValue(); 
    	  int mode = theOscMessage.get(1).intValue();
    	  int dwidth = theOscMessage.get(2).intValue();
    	  int dtip = theOscMessage.get(3).intValue();
    	  int dcol = theOscMessage.get(4).intValue();
    	  int tsize = theOscMessage.get(5).intValue();
    	  int tcol = theOscMessage.get(6).intValue();
    
    	  println("Receiving Config Info For " + configIP);
    	  println("     1. Mode: " + mode);
    	  println("     2. Draw Width: " + dwidth);
    	  println("     3. Draw Tip: " + dtip);
    	  println("     4. Draw Color: " + dcol);
    	  println("     5. Text Size: " + tsize);
    	  println("     6. Text Color: " + tcol);
    	  
    	  if (configIP.equals(myConfiguration.getIP())) {
    	    
    	    //do nothing, we already know our configuration
    	    println("Received our own configuration.  This shouldn't be happening");
    	    
    	  } else {
    	    
            Configuration conf = activePage.getConfigurationByIP(configIP);
            if (conf == null) {
              Configuration tmp = new Configuration(this,configIP,mode, dwidth, dtip,dcol, tsize, tcol);
              activePage.addConfiguration(tmp);
            } else {
              activePage.changeConfiguration(configIP,mode, dwidth, dtip,dcol, tsize, tcol);
            }
          
    	  }
  	  }
	  } else if (theOscMessage.addrPattern().equals("/tablet/configuration/mode")) {
	    String configIP = theOscMessage.get(0).stringValue(); 
      int mode = theOscMessage.get(1).intValue();
      Configuration conf = activePage.getConfigurationByIP(configIP);
      conf.setMode(mode);
      println("Mode changed: " + configIP + " , " + mode);
	  }
	}
	
	
	
	
	
	private void parseKey(OscMessage theOscMessage) {
	  String configIP = theOscMessage.get(0).stringValue();
	  
	  if (configIP.equals(myConfiguration.getIP())) {
	    println("In self's parsekey method, this shouldn't be happening.");
	    return;
	  }
	  
	  char keyEntry = theOscMessage.get(1).charValue();
	  activePage.keyPress(configIP, keyEntry);
  }
	
	
	
	
	// all cursor events are the same, the only difference is the address they are routed to corresponding to the cursor event.
	private void parseCursor(OscMessage theOscMessage) {
	  
	  //println("inside parse cursor " + theOscMessage);
	  
	  String configIP = theOscMessage.get(0).stringValue();  
    float sessID = theOscMessage.get(1).floatValue();
    int curID = theOscMessage.get(2).intValue();
    float xpos = theOscMessage.get(3).floatValue();
    float ypos = theOscMessage.get(4).floatValue();
    
    println("Receiving Cursor Event For " + configIP);
    println("     1. Configuration ID: " + configIP);
    println("     2. Session ID: " + sessID);
    println("     3. Cursor ID: " + curID);
    println("     4. X Position: " + xpos);
    println("     5. Y Position: " + ypos);
	  
    //make new tuio cursor
    long tmpSess = (new Float(sessID)).longValue();
    TuioCursor tcur = new TuioCursor(tmpSess,curID,xpos/width,ypos/height);
    
	  if (theOscMessage.addrPattern().equals("/tablet/cursor/add")) {
	    println("     6. Event: Add");
	    activePage.curAdd(configIP, tcur);
	  } else if (theOscMessage.addrPattern().equals("/tablet/cursor/press")) {
	    println("     6. Event: Press");
      activePage.curPress(configIP, tcur);
	  } else if (theOscMessage.addrPattern().equals("/tablet/cursor/drag")) {
	    println("     6. Event: Drag");
	    activePage.curDrag(configIP, tcur);
	  } else if (theOscMessage.addrPattern().equals("/tablet/cursor/release")) {
	    println("     6. Event: Release");
	    activePage.curRelease(configIP, tcur);
	  }
	  
  }
	
	
	
	private void parsePage(OscMessage theOscMessage) {
	  if ( configurationStatus == 0 ) {
	    println("Receiving page.");
      activePage = new Page(this,theOscMessage.get(0).stringValue());
      myConfiguration = activePage.getConfigurationByIP(myConfiguration.getIP());
	  }
  }
	

	
	
	
	
	
	
	
	

	
	
	
	/** This section control the action module ********************************************************************/
	
	//handles the events when a file is dropped in the app boundries
  public void dropEvent(DropEvent theDropEvent) {
    println("toString()\t"+theDropEvent.toString());
    println("isImage()\t"+theDropEvent.isImage());
    println("isFile()\t"+theDropEvent.isFile());
    println("file()\t"+theDropEvent.file());
    println("isURL()\t"+theDropEvent.isURL());
    println("url()\t"+theDropEvent.url());
    println("dropTargetDropEvent()\t"+theDropEvent.dropTargetDropEvent());
    
    /** Here we look at the filename and examine the last 3 characters to determine the extention,
     *  if the image is a text file then we add that text file to our display string, if its an image
     *  than we display it as an image.  
     */
    
    tObject tmp = null;
    if ( theDropEvent.isImage() ) {
      println("### loading image ...");
      PImage img = theDropEvent.loadImage();
      tmp = new tObject(this,img,new Vec3D(100,100,0),theDropEvent.file());
      activePage.addObject(tmp);
    } else {
      tmp = new tObject(this,theDropEvent.file(),new Vec3D(10,10,0),1);
      activePage.addObject(tmp);
    }
    
  }
	
	
	public void Export() {
	  String savePath = selectOutput("Select a folder to save your image to");  // Opens file chooser
	  if (savePath == null) {
	    // If a file was not selected
	    println("No output file was selected...");
	  } else {
	    save(savePath + ".png");
	  }
	}
	
	
	
	
	
	
	
	
	
	
	/**This area handles the interaction with the application ************************************************************/
	
	boolean pressed = false;
	
	private void synch(OscMessage m) {  
        oscP5.send(m, activePage.getNetAddresses());
	}
	
	private void synchCursors(TuioCursor t, float xpos, float ypos, int event) {
	  OscMessage mess = null;
	  
	  //this could be done better by just prepending,  but i do what i want.  mwahahaha
	  switch(event) {
    	  case(0):
    	    mess = new OscMessage("/tablet/cursor/add");
          break;
    	  case(1): //push
    	    mess = new OscMessage("/tablet/cursor/press");
    	    break;
    	  case(2): //drag
    	    mess = new OscMessage("/tablet/cursor/drag");
    	    break;
    	  case(3): //release
    	    mess = new OscMessage("/tablet/cursor/release");
    	    break;
    	  default:
    	    break;
  	 }
	   
	   mess.add(myConfiguration.getIP());
	   mess.add(t.getSessionID());
	   mess.add(t.getCursorID());
	   mess.add(xpos);
	   mess.add(ypos);
	   synch(mess);
	}
	
	
	
	/** Mouse events are converted into TUIO events so they can be handled the same **/
	TuioCursor damouse;
	public void mousePressed() {
	  Vector tuioCursorList = tuioClient.getTuioCursors();
	  damouse = new TuioCursor( sessionID , tuioCursorList.size(), (float)(mouseX) / (float)width, (float)mouseY / (float)height);
	  addTuioCursor(damouse);  
	  //println("Mouse Pressed: " + damouse.getCursorID());
	}

	public void mouseDragged() {
	  damouse.update( (float)(mouseX) / (float)width, (float)mouseY / (float)height);
	  tuioDrag(damouse);
	  //println("Mouse Dragged " + damouse.getCursorID());
	}

	public void mouseReleased() {
	  removeTuioCursor(damouse);
	  //println("Mouse Released: " + damouse.getCursorID());
	}

	
	/** This section handles this Tablet's cursor events.  
	 */
//called when a cursor is added to the scene
  public void addTuioCursor(TuioCursor tcur) {

    sessionID++;
    tuioPush(tcur);
    
  }
  
	public void tuioPush( TuioCursor tcur ) {

    float tmpX = tcur.getScreenX(width);
    float tmpY = tcur.getScreenY(height);
    int cur = tcur.getCursorID();
    
    //if the mousevents happen in the region of a local view send their events to the gui only
    if( tmpY > gui.getMenuY() || configurationStatus == 0 || gui.menuView == -1) {
      gui.pressEvent(tmpX, tmpY);
    } else {
      activePage.curPress(myConfiguration.getIP(),tcur);
      synchCursors(tcur, tcur.getScreenX(width), tcur.getScreenY(height), 1);
    }
    
	}

	public void tuioDrag(TuioCursor tcur) {
	  
    float tmpX = tcur.getScreenX(width);
    float tmpY = tcur.getScreenY(height);
    int cur = tcur.getCursorID();
    
    if( tmpY > gui.getMenuY() || configurationStatus == 0 || gui.menuView == -1) {
      //do nothing, we'll probably add this back in later for sliders
    } else {
      activePage.curDrag(myConfiguration.getIP(),tcur);
      synchCursors(tcur, tcur.getScreenX(width), tcur.getScreenY(height), 2);
    }
    
	}
	
  // called when a cursor is removed from the scene
  public void removeTuioCursor(TuioCursor tcur) {
    
    float tmpX = tcur.getScreenX(width);
    float tmpY = tcur.getScreenY(height);
    int cur = tcur.getCursorID();
    
    if( tmpY > gui.getMenuY() || configurationStatus == 0 || gui.menuView == -1) {
      gui.releaseEvent(tcur.getScreenX(width), tcur.getScreenY(height));
    } else {
      activePage.curRelease(myConfiguration.getIP(),tcur);
      synchCursors(tcur, tcur.getScreenX(width), tcur.getScreenY(height), 3);
    }
    
  }
  
  //called when a cursor is moved
  public void updateTuioCursor (TuioCursor tcur) {
    tuioDrag(tcur);
  }
  
	// called when an object is added to the scene
	public void addTuioObject(TuioObject tobj) { }

	// called when an object is removed from the scene
	public void removeTuioObject(TuioObject tobj) { }

	// called when an object is moved
	public void updateTuioObject (TuioObject tobj) { }

	// called after each message bundle
	// representing the end of an image frame
	public void refresh(TuioTime bundleTime) { redraw(); }

	
	
	
	
	
	
	
	
	/** This section handles the interpretation of keystrokes **/
	
	private void synchKeystrokes(char c) {
	    OscMessage mess = new OscMessage("/tablet/key");
	    mess.add(myConfiguration.getIP());
	    mess.add(c);
	    synch(mess);
	}

	public void keyPressed() {
	  
	 
	  if(gui.getSessionConfigurationView() > -1) {
	    
	    gui.keyEvent(key);
	    
	  } else {
	    
	    activePage.keyPress(myConfiguration.getIP(), key);
	    synchKeystrokes(key);
	    
	  }
	  
	}
	
	
}	
